package cn.lg.soar.system.biz.modules.monitor.service.impl;

import cn.lg.soar.common.algorithm.CryptoUtils;
import cn.lg.soar.common.algorithm.serial.Snowflake;
import cn.lg.soar.common.util.AssertUtil;
import cn.lg.soar.common.util.DatetimeUtil;
import cn.lg.soar.common.util.IpUtil;
import cn.lg.soar.common.util.ParameterUtil;
import cn.lg.soar.common.util.current.CurrentProxy;
import cn.lg.soar.common.util.current.CurrentUser;
import cn.lg.soar.common.util.current.ThreadLocalHolder;
import cn.lg.soar.common.util.current.UserContext;
import cn.lg.soar.common.util.data.DataUtil;
import cn.lg.soar.common.util.data.RandomUtil;
import cn.lg.soar.common.util.token.SoarJWT;
import cn.lg.soar.core.config.cache.template.ICacheTemplate;
import cn.lg.soar.mvc.util.ServletUtils;
import cn.lg.soar.system.api.enums.OnlineModeEnum;
import cn.lg.soar.system.api.model.AuthProps;
import cn.lg.soar.system.api.model.LoginInfo;
import cn.lg.soar.system.biz.config.SystemProps;
import cn.lg.soar.system.biz.modules.auth.service.ITokenCurrBindService;
import cn.lg.soar.system.biz.modules.monitor.entity.RefreshToken;
import cn.lg.soar.system.biz.modules.monitor.mapper.TokenInfoMapper;
import cn.lg.soar.system.biz.modules.monitor.service.IRefreshTokenService;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import eu.bitwalker.useragentutils.OperatingSystem;
import eu.bitwalker.useragentutils.UserAgent;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;

import javax.servlet.http.HttpServletRequest;
import java.io.Serializable;
import java.time.LocalDateTime;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * refreshToken状态管理(可根据自己的需求修改)
 * @author luguoxiang
 * 开源项目：https://gitee.com/lgx1992/lg-soar 求star！请给我star！请帮我点个star！
 */
public class RefreshTokenServiceImpl extends ServiceImpl<TokenInfoMapper, RefreshToken> implements IRefreshTokenService, CurrentProxy<RefreshTokenServiceImpl> {

    @Autowired
    private ITokenCurrBindService tokenCurrBindService;
    @Autowired
    private ICacheTemplate<String, String> cacheTemplate;
    private SystemProps properties;
    private long accessTokenExpires;
    private long shortRefreshTokenExpires;
    private byte[] refreshTokenSecret;
    private OnlineModeEnum mode;

    @Autowired
    public void setProperties(SystemProps properties) {
        this.properties = properties;
        this.accessTokenExpires = properties.getAccessTokenExpire().getSeconds() * 1000;
        this.shortRefreshTokenExpires = this.accessTokenExpires + 600_000;
        if (properties.getRefreshTokenSecret() == null) {
            throw new RuntimeException("请配置：lg.system.refresh-token-secret");
        }
        this.refreshTokenSecret = properties.getRefreshTokenSecret().getBytes();
        this.mode = properties.getOnlineMode();
    }

    @Override
    public AuthProps getAccessTokenConfig() {
        return properties;
    }


    @Override
    public String create(LoginInfo loginInfo, long expires) {
        HttpServletRequest request = ServletUtils.getRequest();
        int ip = IpUtil.getRemoteIpInt(request);
        long tokenValue = RandomUtil.getLong();
        long tokenId = Snowflake.nextValue();
        Long userId = loginInfo.getUserId();
        Integer userType = loginInfo.getUserType();
        if (userType == null) {
            userType = 0;
        }
        Integer clientType = loginInfo.getClientType();
        if (clientType == null) {
            clientType = 0;
        }
        Integer tenantId = loginInfo.getTenantId();
        if (tenantId == null) {
            tenantId = 0;
        }
        String[] otherInfo = loginInfo.getOtherInfo();
        // 同时在线控制
        switch (mode) {
            case only:
                remove(
                        Wrappers.<RefreshToken>lambdaQuery()
                                .eq(RefreshToken::getUserType, userType)
                                .eq(RefreshToken::getUserId, userId)
                );
                break;
            case clientType:
                remove(
                        Wrappers.<RefreshToken>lambdaQuery()
                                .eq(RefreshToken::getUserType, userType)
                                .eq(RefreshToken::getUserId, userId)
                                .eq(RefreshToken::getClientType, clientType)
                );
                break;
            default:break;
        }

        // 构造载体
        Object[] payload;
        final int LEN = 7;
        if (otherInfo == null) {
            payload = new Object[LEN];
            payload[0] = DataUtil.longToBase64(tokenId);
            payload[1] = DataUtil.longToBase64(tokenValue);
            payload[2] = userId.toString();
            payload[3] = userType;
            payload[4] = tenantId;
            payload[5] = clientType;
            payload[6] = DataUtil.longToBase64(expires);
        } else {
            payload = new Object[LEN + otherInfo.length];
            payload[0] = DataUtil.longToBase64(tokenId);
            payload[1] = DataUtil.longToBase64(tokenValue);
            payload[2] = userId.toString();
            payload[3] = userType;
            payload[4] = tenantId;
            payload[5] = clientType;
            payload[6] = DataUtil.longToBase64(expires);
            for (int i = 0; i < otherInfo.length; i++) {
                payload[LEN + i] = otherInfo[i];
            }
        }
        UserAgent userAgent = UserAgent.parseUserAgentString(request.getHeader("User-Agent"));
        OperatingSystem operatingSystem = userAgent.getOperatingSystem();
        // 生成token
        String refreshToken = new SoarJWT(properties.getRefreshTokenSecret(), expires).create(payload);
        // 入库
        long millis = System.currentTimeMillis();
        RefreshToken refreshTokenInfo = new RefreshToken();
        refreshTokenInfo.setId(tokenId);
        refreshTokenInfo.setValue(tokenValue);
        refreshTokenInfo.setUserType(userType);
        refreshTokenInfo.setUserId(userId);
        refreshTokenInfo.setClientType(clientType);
        refreshTokenInfo.setIp(ip);
        refreshTokenInfo.setSystem(operatingSystem.getName());
        refreshTokenInfo.setBrowser(userAgent.getBrowser().getName());
        refreshTokenInfo.setExpires(DatetimeUtil.from(millis + expires));
        refreshTokenInfo.setTokenExpires(DatetimeUtil.from(millis + accessTokenExpires));
        AssertUtil.isTrue(this.save(refreshTokenInfo), "保存token信息失败");
        // 记录登录信息
        ThreadLocalHolder.setCurrentUser(CurrentUser.valueOf(payload));
        // 3.返回信息
        return refreshToken;
    }

    @Override
    public String create(LoginInfo loginInfo, boolean rememberMe) {
        Integer clientType = loginInfo.getClientType();
        long expires;
        if (rememberMe) {
            if (clientType == null) {
                expires = properties.getRefreshTokenExpireMillis();
            } else {
                expires = properties.getRefreshTokenExpireMillis(clientType);
            }
        } else {
            expires = shortRefreshTokenExpires;
        }
        // 创建刷新token
        return create(loginInfo, expires);
    }

    @Override
    public String create(Long userId, long expires) {
        LoginInfo loginInfo = new LoginInfo();
        loginInfo.setUserType(0);
        loginInfo.setUserId(userId);
        return create(loginInfo, expires);
    }

    @Override
    public String create(Long userId, boolean rememberMe) {
        LoginInfo loginInfo = new LoginInfo();
        loginInfo.setUserType(0);
        loginInfo.setUserId(userId);
        return create(loginInfo, rememberMe);
    }

    @Override
    public RefreshToken verify(String refreshToken) {
        // 验证token串有效性
        SoarJWT.Result result = new SoarJWT.Decoder(CryptoUtils.HMAC256E(this.refreshTokenSecret))
                .verify(refreshToken);
        ParameterUtil.isTrue(result.isValid(), "refresh token 无效");
        // 准备数据
        Object[] payload = SoarJWT.parsePayload(refreshToken, Object[].class);
        // 验证token状态
        RefreshToken tokenInfo = getById(DataUtil.base64ToLong((String) payload[0]));
        ParameterUtil.notNull(tokenInfo, "refresh token 无效");
        // 验证tokenInfo是否属于当前token
        ParameterUtil.equals(tokenInfo.getValue(), DataUtil.base64ToLong((String) payload[1]), "refresh token 无效");
        return tokenInfo;
    }

    @Override
    public String refresh(String refreshToken) {
        // 验证
        RefreshToken token = verify(refreshToken);
        Object[] payload = SoarJWT.parsePayload(refreshToken, Object[].class);
        // 有效期
        long expires = DataUtil.base64ToLong((String) payload[6]);
        // 更新tokenValue
        long tokenValue = RandomUtil.getLong();
        // 载体更新
        payload[1] = DataUtil.longToBase64(tokenValue);
        // ip
        HttpServletRequest request = ServletUtils.getRequest();
        int ip = IpUtil.getRemoteIpInt(request);
        // 入库
        long millis = System.currentTimeMillis();
        RefreshToken refreshTokenInfo = new RefreshToken();
        refreshTokenInfo.setId(token.getId());
        refreshTokenInfo.setValue(tokenValue);
        refreshTokenInfo.setIp(ip);
        refreshTokenInfo.setExpires(DatetimeUtil.from(millis + expires));
        refreshTokenInfo.setTokenExpires(DatetimeUtil.from(millis + accessTokenExpires));
        AssertUtil.isTrue(this.updateById(refreshTokenInfo), "保存token信息失败");
        // 记录登录信息
        ThreadLocalHolder.setCurrentUser(CurrentUser.valueOf(payload));
        // 生成新的token
        return new SoarJWT(properties.getRefreshTokenSecret(), expires).create(payload);
    }

    @Override
    public String refresh(Object[] payload) {
        long tokenId = Long.parseLong((String) payload[0]);
        long tokenValue = RandomUtil.getLong();
        payload[1] = DataUtil.longToBase64(tokenValue);
        // 有效期
        long expires = DataUtil.base64ToLong((String) payload[6]);
        // ip
        HttpServletRequest request = ServletUtils.getRequest();
        int ip = IpUtil.getRemoteIpInt(request);
        // 入库
        long millis = System.currentTimeMillis();
        RefreshToken refreshTokenInfo = new RefreshToken();
        refreshTokenInfo.setId(tokenId);
        refreshTokenInfo.setValue(tokenValue);
        refreshTokenInfo.setIp(ip);
        refreshTokenInfo.setExpires(DatetimeUtil.from(millis + expires));
        refreshTokenInfo.setTokenExpires(DatetimeUtil.from(millis + accessTokenExpires));
        AssertUtil.isTrue(this.updateById(refreshTokenInfo), "保存token信息失败");
        // 生成新的token
        return new SoarJWT(properties.getRefreshTokenSecret(), expires).create(payload);
    }

    @Override
    public void clear() {
        CurrentUser currentUser = (CurrentUser) UserContext.getUser();
        AssertUtil.isTrue(removeById(currentUser.getTokenId()), "操作失败");
    }

    @Override
    public void clear(Long userId, Collection<Integer> clientTypes) {
        LambdaQueryWrapper<RefreshToken> wrapper = Wrappers.<RefreshToken>lambdaQuery()
                .eq(RefreshToken::getUserId, userId);
        if (CollectionUtils.isNotEmpty(clientTypes)) {
            wrapper.in(RefreshToken::getClientType, clientTypes);
        }
        remove(wrapper);
    }

    @Override
    public boolean removeByIds(Collection<?> ids) {
        AssertUtil.notEmpty(ids, "缺少id参数");
        AssertUtil.isTrue(super.removeByIds(ids), "清除token失败");
        return true;
    }

    @Override
    public Set<String> removeByUserId(Serializable userId) {
        List<Object> objects = this.listObjs(Wrappers.<RefreshToken>lambdaQuery()
                .select(RefreshToken::getId)
                .eq(RefreshToken::getUserId, userId));
        if (CollectionUtils.isEmpty(objects)) {
            return Collections.emptySet();
        }
        Set<String> ids = objects.stream().map(Object::toString).collect(Collectors.toSet());
        removeByIds(ids);
        return ids;
    }

    @Override
    public Set<String> removeByUserId(Serializable userId, Integer clientType) {
        List<Object> objects = this.listObjs(Wrappers.<RefreshToken>lambdaQuery()
                .select(RefreshToken::getId)
                .eq(RefreshToken::getUserId, userId)
                .eq(RefreshToken::getClientType, clientType)
        );
        Set<String> ids = objects.stream().map(Object::toString).collect(Collectors.toSet());
        this.remove(
                Wrappers.<RefreshToken>lambdaQuery()
                        .in(RefreshToken::getId, ids)
                        .or()
                        .le(RefreshToken::getExpires, LocalDateTime.now())
        );
        return ids;
    }

    @Override
    @Scheduled(fixedDelay = 610000)
    public void optimize() {
        // 利用缓存（当使用redis时）实现部署多个微服务实例时，每十分钟只会执行一次
        String s = cacheTemplate.get("refresh-token-optimize");
        if (s == null) {
            List<String> ids = listObjs(
                    Wrappers.<RefreshToken>lambdaQuery()
                            .select(RefreshToken::getId)
                            .le(RefreshToken::getExpires, LocalDateTime.now()),
                    Object::toString
            );
            if (!ids.isEmpty()) {
                tokenCurrBindService.removeByIds(ids);
                this.removeByIds(ids);
            }
            cacheTemplate.put("refresh-token-optimize", "1", 600000);
        }
    }
}
